Last Updated: December 19, 2025
Spotify is a popular digital music streaming platform that gives users access to a vast library of songs, albums, podcasts, and audio content from artists and creators around the world.
Playlist
The hottest tracks right now
Spotify • 6 songs
Blinding Lights
The Weeknd
Levitating
Dua Lipa, DaBaby
Save Your Tears
The Weeknd
Stay
Justin Bieber, The Kid LAROI
Peaches
Justin Bieber, Daniel Caesar
Montero
Lil Nas X
It offers features such as:
In this chapter, we will explore the low-level design of a Spotify like service in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a conversation between the candidate and the interviewer might unfold:
Candidate: Should we support both free and premium users?
Interviewer: Yes. Free users should experience occasional ads during playback. Premium users should enjoy uninterrupted, ad-free listening.
Candidate: Do users interact only with individual songs, or can they also play albums and playlists?
Interviewer: Users should be able to play individual songs, albums, and playlists. These should be handled uniformly.
Candidate: Should users be able to follow artists and get notified when a new album is released?
Interviewer: Yes. Users can follow artists and should receive notifications whenever those artists release new albums.
Candidate: Should the music player support playback controls like play, pause, and next track?
Interviewer: Yes. The user should be able to control playback using these actions. The player should behave differently based on its current state (stopped, playing, paused).
Candidate: Is search functionality in scope?
Interviewer: Yes. Users should be able to search for songs by title and artists by name.
Candidate: Do we want to show song recommendations?
Interviewer: Yes. Include a simple recommendation engine. For now, simulate genre-based or randomized recommendations.
After gathering the details, we can summarize the key system requirements.
Core entities are the foundational building blocks of our system. We identify them by analyzing key nouns (e.g., song, album, artist, playlist, user, playback history) and actions (e.g., stream, search, add, browse, reorder) from the functional requirements. These typically translate directly into classes, enums, or interfaces in an object-oriented design.
Let’s walk through the requirements and extract the relevant entities:
This points to the core content entities: Song, Album, and Playlist. The need to treat them "uniformly" is a strong indicator for the Composite design pattern. This leads to a Playable interface that all three content types implement, allowing the Player to handle them interchangeably.
This suggests a central Player entity to manage the playback queue and state.
The system needs a User entity to represent the listener. The difference in playback (e.g., with or without ads) is a behavioral concern, which is handled by the Strategy pattern. This introduces a PlaybackStrategy interface with different implementations (FreePlaybackStrategy, PremiumPlaybackStrategy). A SubscriptionTier enum (FREE, PREMIUM) is used to determine which strategy a User gets.
This introduces the Artist entity.
These cross-cutting concerns are best encapsulated in dedicated service classes. A SearchService is needed to handle queries for songs and artists. Similarly, a RecommendationService is responsible for generating song suggestions.
To manage the interactions between all these components, an MusicStreamingSystem class acts as a Facade and Singleton. It provides a simple entry point to interact with the entire system.
Playable (Interface): The core abstraction of the Composite pattern, allowing Song, Album, and Playlist to be treated uniformly by the player.Song, Album, Playlist: Concrete Playable entities that represent the music content in the system.User: Represents a listener in the system. Holds a PlaybackStrategy based on their subscription.Artist: Represents the music creator. Acts as a Subject that notifies its followers of new releases.Player: The central playback engine that manages the music queue and current song.Services (SearchService, RecommendationService): Classes that encapsulate business logic for searching the catalog and generating recommendations.Enums (SubscriptionTier, PlayerStatus): Define fixed sets of constants for user types and player states, ensuring consistency.MusicStreamingSystem: A Facade and Singleton that provides a simplified, high-level API to the entire system.These core entities define the essential abstractions of a music streaming service like Spotify and will guide the structure of your low-level design and class diagrams.
This section breaks down the system's architecture into its fundamental classes, their responsibilities, and the relationships that connect them. We also explore the key design patterns that provide robustness and flexibility to the solution.
The system is composed of several types of classes, each with a distinct role.
SubscriptionTier: Defines the user's subscription level (FREE, PREMIUM), which dictates their playback experience.PlayerStatus: Represents the current state of the music player (PLAYING, PAUSED, STOPPED).SongA data class representing an individual music track.
It holds details like title, artist, and duration. It also implements the Playable interface, making it a leaf node in the Composite pattern.
AlbumA data class that acts as a collection of Songs. It implements the Playable interface, making it a composite node.
PlaylistA data class representing a user-curated list of Songs. It also implements the Playable interface, acting as another composite node.
Playable (Interface)The Component role in the Composite pattern. It defines a common interface (getTracks) for both individual songs and collections (albums, playlists), allowing them to be treated uniformly.
ArtistRepresents a music artist or band.
It acts as a concrete Subject, maintaining a list of followers (observers) and notifying them when a new Album is released.
UserRepresents a listener on the platform.
It acts as a concrete Observer by implementing ArtistObserver, allowing it to "follow" artists. It is configured with a PlaybackStrategy based on its subscription tier. Its construction is handled by a nested Builder.
PlayerThe core music player.
It acts as the Context for the State pattern, delegating actions like play and pause to its current PlayerState object. It manages the playback queue.
SearchService & RecommendationServiceService-layer classes that encapsulate specific business logic for searching the catalog and generating recommendations.
MusicStreamingSystem (Singleton & Facade)The primary entry point for the application.
It provides a simple, unified API to the client, hiding the complex interactions between the various services, data models, and patterns.
The relationships between classes define the system's structure and data flow.
MusicStreamingSystem "has-a" collection of Users, Songs, and Artists, managing their lifecycle.Album is composed of a list of Songs.Playlist is composed of a list of Songs.Player is associated with a single PlayerState at any given time.User is associated with a PlaybackStrategy.Artist (Subject) is associated with a list of ArtistObservers (Users).Song is associated with its Artist.Command object holds a reference to the Player instance (the receiver) on which it will execute.Song, Album, and Playlist all implement the Playable interface.Artist extends the abstract Subject class.User implements the ArtistObserver interface.PlayingState, etc.) implement the PlayerState interface.FreePlaybackStrategy, etc.) implement the PlaybackStrategy interface.PlayCommand, etc.) implement the Command interface.MusicStreamingDemo) depends on the MusicStreamingSystem facade to interact with the system.Player depends on a PlaybackStrategy (provided by the User) to play a song.User.Builder depends on the PlaybackStrategy.getStrategy factory method.This pattern is used to make core algorithms interchangeable.
The PlaybackStrategy allows the playback behavior (ad-free vs. ad-supported) to be assigned to a User dynamically based on their subscription tier.
The RecommendationStrategy allows for different recommendation algorithms to be swapped out easily.
The lifecycle of the Player is managed using the State pattern. The Player (Context) delegates its behavior to different PlayerState objects (PlayingState, PausedState, StoppedState). This cleanly separates state-specific logic and makes managing player actions robust.
This pattern is used for the "follow artist" feature. The Artist (Subject) notifies all subscribed Users (Observers) when a new album is released, decoupling the artist's actions from the user notification system.
The Playable interface allows the Player to treat individual Songs (leafs) and collections like Albums or Playlists (composites) uniformly. The player.load() method can accept any Playable object without needing to know its specific type.
This pattern encapsulates a player action (e.g., "play", "pause") into a standalone object (PlayCommand, PauseCommand). This decouples the client that issues the request (e.g., a UI button) from the Player object that knows how to perform it.
The User.Builder provides a fluent, step-by-step API for constructing a User object, especially useful for setting up the correct PlaybackStrategy based on subscription details.
The PlaybackStrategy.getStrategy() method acts as a simple factory, encapsulating the logic for creating the correct strategy instance based on the user's SubscriptionTier.
The MusicStreamingSystem class serves as a facade. It provides a simple, high-level API (registerUser, searchSongsByTitle, getPlayer) that hides the complex internal workflows involving players, states, strategies, and observers.
MusicStreamingSystem is implemented as a singleton to ensure a single, globally accessible point of control for the entire application, managing all users, content, and services.
1class SubscriptionTier(Enum):
2 FREE = "FREE"
3 PREMIUM = "PREMIUM"
4
5class PlayerStatus(Enum):
6 PLAYING = "PLAYING"
7 PAUSED = "PAUSED"
8 STOPPED = "STOPPED"SubscriptionTier: Determines the user’s playback strategy (ad-supported vs ad-free).PlayerStatus: Reflects the current state of the player.Song implements Playable, enabling uniform treatment alongside Album and Playlist. This demonstrates the Composite pattern for treating individual songs and groups (albums/playlists) interchangeably.
1class Playable(ABC):
2 @abstractmethod
3 def get_tracks(self) -> List['Song']:
4 pass1class Song(Playable):
2 def __init__(self, song_id: str, title: str, artist: 'Artist', duration_in_seconds: int):
3 self._id = song_id
4 self._title = title
5 self._artist = artist
6 self._duration_in_seconds = duration_in_seconds
7
8 def get_tracks(self) -> List['Song']:
9 return [self]
10
11 def __str__(self) -> str:
12 return f"'{self._title}' by {self._artist.get_name()}"
13
14 @property
15 def id(self) -> str:
16 return self._id
17
18 @property
19 def title(self) -> str:
20 return self._title
21
22 @property
23 def artist(self) -> 'Artist':
24 return self._artist1class Album(Playable):
2 def __init__(self, title: str):
3 self._title = title
4 self._tracks: List[Song] = []
5
6 def add_track(self, song: Song):
7 self._tracks.append(song)
8
9 def get_tracks(self) -> List[Song]:
10 return self._tracks.copy()
11
12 def get_title(self) -> str:
13 return self._title1class Playlist(Playable):
2 def __init__(self, name: str):
3 self._name = name
4 self._tracks: List[Song] = []
5
6 def add_track(self, song: Song):
7 self._tracks.append(song)
8
9 def get_tracks(self) -> List[Song]:
10 return self._tracks.copy()This pattern allows users to "follow" an artist and receive notifications when that artist releases a new album.
1class ArtistObserver(ABC):
2 @abstractmethod
3 def update(self, artist: 'Artist', new_album: 'Album'):
4 pass
5
6class Subject:
7 def __init__(self):
8 self._observers: List[ArtistObserver] = []
9
10 def add_observer(self, observer: ArtistObserver):
11 self._observers.append(observer)
12
13 def remove_observer(self, observer: ArtistObserver):
14 if observer in self._observers:
15 self._observers.remove(observer)
16
17 def notify_observers(self, artist: 'Artist', album: 'Album'):
18 for observer in self._observers:
19 observer.update(artist, album)1class Artist(Subject):
2 def __init__(self, artist_id: str, name: str):
3 super().__init__()
4 self._id = artist_id
5 self._name = name
6 self._discography: List['Album'] = []
7
8 def release_album(self, album: 'Album'):
9 self._discography.append(album)
10 print(f"[System] Artist {self._name} has released a new album: {album.get_title()}")
11 self.notify_observers(self, album)
12
13 @property
14 def id(self) -> str:
15 return self._id
16
17 def get_name(self) -> str:
18 return self._name1class User(ArtistObserver):
2 def __init__(self, user_id: str, name: str, playback_strategy: PlaybackStrategy):
3 self._id = user_id
4 self._name = name
5 self._playback_strategy = playback_strategy
6 self._followed_artists: Set[Artist] = set()
7
8 def follow_artist(self, artist: Artist):
9 self._followed_artists.add(artist)
10 artist.add_observer(self)
11
12 def update(self, artist: Artist, new_album: Album):
13 print(f"[Notification for {self._name}] Your followed artist {artist.get_name()} "
14 f"just released a new album: {new_album.get_title()}!")
15
16 @property
17 def playback_strategy(self) -> PlaybackStrategy:
18 return self._playback_strategy
19
20 @property
21 def id(self) -> str:
22 return self._id
23
24 def get_name(self) -> str:
25 return self._name
26
27 class Builder:
28 def __init__(self, name: str):
29 self._id = str(uuid.uuid4())
30 self._name = name
31 self._playback_strategy = None
32
33 def with_subscription(self, tier: SubscriptionTier, songs_played: int) -> 'User.Builder':
34 self._playback_strategy = PlaybackStrategy.get_strategy(tier, songs_played)
35 return self
36
37 def build(self) -> 'User':
38 return User(self._id, self._name, self._playback_strategy)The Artist (Subject) doesn't know anything about the User class. It only knows it has a list of ArtistObserver objects to notify. This decouples the content creators from the content consumers.
The Player class holds the current state and delegates all actions to it.
1class Player:
2 def __init__(self):
3 self._state = StoppedState()
4 self._status = PlayerStatus.STOPPED
5 self._queue: List[Song] = []
6 self._current_index = -1
7 self._current_song: Optional[Song] = None
8 self._current_user: Optional[User] = None
9
10 def load(self, playable: Playable, user: User):
11 self._current_user = user
12 self._queue = playable.get_tracks()
13 self._current_index = 0
14 print(f"Loaded {len(self._queue)} tracks for user {user.get_name()}.")
15 self._state = StoppedState()
16
17 def play_current_song_in_queue(self):
18 if 0 <= self._current_index < len(self._queue):
19 song_to_play = self._queue[self._current_index]
20 self._current_user.playback_strategy.play(song_to_play, self)
21
22 def click_play(self):
23 self._state.play(self)
24
25 def click_pause(self):
26 self._state.pause(self)
27
28 def click_next(self):
29 if self._current_index < len(self._queue) - 1:
30 self._current_index += 1
31 self.play_current_song_in_queue()
32 else:
33 print("End of queue.")
34 self._state.stop(self)
35
36 def change_state(self, state: PlayerState):
37 self._state = state
38
39 def set_status(self, status: PlayerStatus):
40 self._status = status
41
42 def set_current_song(self, song: Song):
43 self._current_song = song
44
45 def has_queue(self) -> bool:
46 return len(self._queue) > 0The player's behavior changes drastically based on whether it is Playing, Paused, or Stopped. The State pattern is perfect for managing these transitions cleanly.
1class PlayerState(ABC):
2 @abstractmethod
3 def play(self, player: 'Player'):
4 pass
5
6 @abstractmethod
7 def pause(self, player: 'Player'):
8 pass
9
10 @abstractmethod
11 def stop(self, player: 'Player'):
12 pass
13
14class PausedState(PlayerState):
15 def play(self, player: 'Player'):
16 print("Resuming playback.")
17 player.change_state(PlayingState())
18 player.set_status(PlayerStatus.PLAYING)
19
20 def pause(self, player: 'Player'):
21 print("Already paused.")
22
23 def stop(self, player: 'Player'):
24 print("Stopping playback from paused state.")
25 player.change_state(StoppedState())
26 player.set_status(PlayerStatus.STOPPED)
27
28class PlayingState(PlayerState):
29 def play(self, player: 'Player'):
30 print("Already playing.")
31
32 def pause(self, player: 'Player'):
33 print("Pausing playback.")
34 player.change_state(PausedState())
35 player.set_status(PlayerStatus.PAUSED)
36
37 def stop(self, player: 'Player'):
38 print("Stopping playback.")
39 player.change_state(StoppedState())
40 player.set_status(PlayerStatus.STOPPED)
41
42class StoppedState(PlayerState):
43 def play(self, player: 'Player'):
44 if player.has_queue():
45 print("Starting playback.")
46 player.change_state(PlayingState())
47 player.set_status(PlayerStatus.PLAYING)
48 player.play_current_song_in_queue()
49 else:
50 print("Queue is empty. Load songs to play.")
51
52 def pause(self, player: 'Player'):
53 print("Cannot pause. Player is stopped.")
54
55 def stop(self, player: 'Player'):
56 print("Already stopped.")Each state class encapsulates the logic for that specific state. For example, clickPlay() in the PlayingState does nothing, but in the StoppedState, it starts the playback and transitions the player to the PlayingState. This avoids a large, complex if/else block in the Player class.
The playback experience differs significantly for FREE vs. PREMIUM users. The Strategy pattern allows us to define these different behaviors and assign them to users at runtime.
1class PlaybackStrategy(ABC):
2 @abstractmethod
3 def play(self, song: Song, player: 'Player'):
4 pass
5
6 @staticmethod
7 def get_strategy(tier: SubscriptionTier, songs_played: int) -> 'PlaybackStrategy':
8 if tier == SubscriptionTier.PREMIUM:
9 return PremiumPlaybackStrategy()
10 else:
11 return FreePlaybackStrategy(songs_played)
12
13class FreePlaybackStrategy(PlaybackStrategy):
14 SONGS_BEFORE_AD = 3
15
16 def __init__(self, initial_songs_played: int):
17 self._songs_played = initial_songs_played
18
19 def play(self, song: Song, player: 'Player'):
20 if self._songs_played > 0 and self._songs_played % self.SONGS_BEFORE_AD == 0:
21 print("\n>>> Playing Advertisement: 'Buy Spotify Premium for ad-free music!' <<<\n")
22 player.set_current_song(song)
23 print(f"Free User is now playing: {song}")
24 self._songs_played += 1
25
26class PremiumPlaybackStrategy(PlaybackStrategy):
27 def play(self, song: Song, player: 'Player'):
28 player.set_current_song(song)
29 print(f"Premium User is now playing: {song}")Each strategy class encapsulates a different playback algorithm. FreePlaybackStrategy includes logic for inserting ads, while PremiumPlaybackStrategy does not.
Applies the Strategy pattern to generate different types of song recommendations.
1class RecommendationStrategy(ABC):
2 @abstractmethod
3 def recommend(self, all_songs: List[Song]) -> List[Song]:
4 pass
5
6class GenreBasedRecommendationStrategy(RecommendationStrategy):
7 def recommend(self, all_songs: List[Song]) -> List[Song]:
8 print("Generating genre-based recommendations (simulated)...")
9 shuffled = all_songs.copy()
10 random.shuffle(shuffled)
11 return shuffled[:5]This pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
1class Command(ABC):
2 @abstractmethod
3 def execute(self):
4 pass
5
6class PlayCommand(Command):
7 def __init__(self, player: Player):
8 self._player = player
9
10 def execute(self):
11 self._player.click_play()
12
13class PauseCommand(Command):
14 def __init__(self, player: Player):
15 self._player = player
16
17 def execute(self):
18 self._player.click_pause()
19
20class NextTrackCommand(Command):
21 def __init__(self, player: Player):
22 self._player = player
23
24 def execute(self):
25 self._player.click_next()Each Command object encapsulates a single action (e.g., "play"). This is useful for creating UI elements like buttons. A "Play" button can be configured with a PlayCommand object, and its onClick handler would simply call command.execute(). The button doesn't need to know anything about the Player's internal workings.
1class RecommendationService:
2 def __init__(self, strategy: RecommendationStrategy):
3 self._strategy = strategy
4
5 def set_strategy(self, strategy: RecommendationStrategy):
6 self._strategy = strategy
7
8 def generate_recommendations(self, all_songs: List[Song]) -> List[Song]:
9 return self._strategy.recommend(all_songs)Encapsulates searching and recommending functionality for the catalog.
1class SearchService:
2 def search_songs_by_title(self, songs: List[Song], query: str) -> List[Song]:
3 return [s for s in songs if query.lower() in s.title.lower()]
4
5 def search_artists_by_name(self, artists: List[Artist], query: str) -> List[Artist]:
6 return [a for a in artists if query.lower() in a.get_name().lower()]MusicStreamingSystem (Facade + Singleton)1class MusicStreamingSystem:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __new__(cls):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance._initialized = False
11 return cls._instance
12
13 def __init__(self):
14 if not self._initialized:
15 self._users: Dict[str, User] = {}
16 self._songs: Dict[str, Song] = {}
17 self._artists: Dict[str, Artist] = {}
18 self._player = Player()
19 self._search_service = SearchService()
20 self._recommendation_service = RecommendationService(GenreBasedRecommendationStrategy())
21 self._initialized = True
22
23 @classmethod
24 def get_instance(cls):
25 return cls()
26
27 def register_user(self, user: User):
28 self._users[user.id] = user
29
30 def add_song(self, song_id: str, title: str, artist_id: str, duration: int) -> Song:
31 song = Song(song_id, title, self._artists[artist_id], duration)
32 self._songs[song.id] = song
33 return song
34
35 def add_artist(self, artist: Artist):
36 self._artists[artist.id] = artist
37
38 def search_songs_by_title(self, title: str) -> List[Song]:
39 return self._search_service.search_songs_by_title(list(self._songs.values()), title)
40
41 def get_song_recommendations(self) -> List[Song]:
42 return self._recommendation_service.generate_recommendations(list(self._songs.values()))
43
44 def get_player(self) -> Player:
45 return self._playerMusicStreamingDemoThe demo class validates the entire system by simulating various user interactions.
1class MusicStreamingDemo:
2 @staticmethod
3 def main():
4 system = MusicStreamingSystem.get_instance()
5
6 # --- Setup Catalog ---
7 daft_punk = Artist("art1", "Daft Punk")
8 system.add_artist(daft_punk)
9
10 discovery = Album("Discovery")
11 s1 = system.add_song("s1", "One More Time", daft_punk.id, 320)
12 s2 = system.add_song("s2", "Aerodynamic", daft_punk.id, 212)
13 s3 = system.add_song("s3", "Digital Love", daft_punk.id, 301)
14 s4 = system.add_song("s4", "Radioactive", daft_punk.id, 311)
15 discovery.add_track(s1)
16 discovery.add_track(s2)
17 discovery.add_track(s3)
18 discovery.add_track(s4)
19
20 # --- Register Users (Builder Pattern) ---
21 free_user = User.Builder("Alice").with_subscription(SubscriptionTier.FREE, 0).build()
22 premium_user = User.Builder("Bob").with_subscription(SubscriptionTier.PREMIUM, 0).build()
23 system.register_user(free_user)
24 system.register_user(premium_user)
25
26 # --- Observer Pattern: User follows artist ---
27 print("--- Observer Pattern Demo ---")
28 premium_user.follow_artist(daft_punk)
29 daft_punk.release_album(discovery) # This will notify Bob
30 print()
31
32 # --- Strategy Pattern: Playback behavior ---
33 print("--- Strategy Pattern (Free vs Premium) & State Pattern (Player) Demo ---")
34 player = system.get_player()
35 player.load(discovery, free_user)
36
37 # --- Command Pattern: Controlling the player ---
38 play = PlayCommand(player)
39 pause = PauseCommand(player)
40 next_track = NextTrackCommand(player)
41
42 play.execute() # Plays song 1
43 next_track.execute() # Plays song 2
44 pause.execute() # Pauses song 2
45 play.execute() # Resumes song 2
46 next_track.execute() # Plays song 3
47 next_track.execute() # Plays song 4 (ad for free user)
48 print()
49
50 # --- Premium user experience (no ads) ---
51 print("--- Premium User Experience ---")
52 player.load(discovery, premium_user)
53 play.execute()
54 next_track.execute()
55 print()
56
57 # --- Composite Pattern: Play a playlist ---
58 print("--- Composite Pattern Demo ---")
59 my_playlist = Playlist("My Awesome Mix")
60 my_playlist.add_track(s3) # Digital Love
61 my_playlist.add_track(s1) # One More Time
62
63 player.load(my_playlist, premium_user)
64 play.execute()
65 next_track.execute()
66 print()
67
68 # --- Search and Recommendation ---
69 print("--- Search and Recommendation Service Demo ---")
70 search_results = system.search_songs_by_title("love")
71 print(f"Search results for 'love': {search_results}")
72
73 recommendations = system.get_song_recommendations()
74 print(f"Your daily recommendations: {recommendations}")
75
76if __name__ == "__main__":
77 MusicStreamingDemo.main()Which entity allows users to manage and organize collections of songs in a music streaming service design?
No comments yet. Be the first to comment!